package nl.knaw.huygens.alexandria.endpoint.webannotation; /* * #%L * alexandria-main * ======= * Copyright (C) 2015 - 2017 Huygens ING (KNAW) * ======= * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import static nl.knaw.huygens.alexandria.api.w3c.WebAnnotationConstants.ALLOWED_METHODS; import static nl.knaw.huygens.alexandria.api.w3c.WebAnnotationConstants.ANNOTATION_TYPE_URI; import static nl.knaw.huygens.alexandria.api.w3c.WebAnnotationConstants.DEFAULT_PROFILE; import static nl.knaw.huygens.alexandria.api.w3c.WebAnnotationConstants.JSONLD_MEDIATYPE; import static nl.knaw.huygens.alexandria.api.w3c.WebAnnotationConstants.RESOURCE_TYPE_URI; import static nl.knaw.huygens.alexandria.api.w3c.WebAnnotationConstants.WEBANNOTATION_TYPE; import java.io.IOException; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.jsonldjava.core.JsonLdError; import com.github.jsonldjava.core.JsonLdOptions; import com.github.jsonldjava.core.JsonLdProcessor; import com.github.jsonldjava.utils.JsonUtils; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import io.swagger.annotations.Api; import nl.knaw.huygens.alexandria.api.EndpointPaths; import nl.knaw.huygens.alexandria.api.model.AlexandriaState; import nl.knaw.huygens.alexandria.api.model.search.AlexandriaQuery; import nl.knaw.huygens.alexandria.api.model.w3c.WebAnnotationPrototype; import nl.knaw.huygens.alexandria.config.AlexandriaConfiguration; import nl.knaw.huygens.alexandria.endpoint.JSONEndpoint; import nl.knaw.huygens.alexandria.endpoint.LocationBuilder; import nl.knaw.huygens.alexandria.endpoint.UUIDParam; import nl.knaw.huygens.alexandria.endpoint.search.SearchResult; import nl.knaw.huygens.alexandria.exception.NotFoundException; import nl.knaw.huygens.alexandria.jaxrs.ThreadContext; import nl.knaw.huygens.alexandria.model.AlexandriaAnnotation; import nl.knaw.huygens.alexandria.model.AlexandriaAnnotationBody; import nl.knaw.huygens.alexandria.model.AlexandriaProvenance; import nl.knaw.huygens.alexandria.model.TentativeAlexandriaProvenance; import nl.knaw.huygens.alexandria.service.AlexandriaService; @Path(EndpointPaths.WEB_ANNOTATIONS) @Api("webannotations") public class WebAnnotationsEndpoint extends JSONEndpoint { private final AlexandriaService service; private LocationBuilder locationBuilder; private AlexandriaConfiguration config; private WebAnnotationService webAnnotationService; @Inject public WebAnnotationsEndpoint(AlexandriaService service, // LocationBuilder locationBuilder, // AlexandriaConfiguration config) { this.service = service; this.locationBuilder = locationBuilder; this.config = config; this.webAnnotationService = new WebAnnotationService(service, config); } @POST @Consumes(JSONLD_MEDIATYPE) @Produces(JSONLD_MEDIATYPE) public Response addWebAnnotation(// @HeaderParam("Accept") String acceptHeader, // WebAnnotationPrototype prototype// ) { String expectedProfile = extractProfile(acceptHeader); prototype.setCreated(Instant.now().toString()); WebAnnotation webAnnotation = webAnnotationService.validateAndStore(prototype); String profiledWebAnnotation = profile(webAnnotation.json(), expectedProfile); return Response.created(webAnnotationService.webAnnotationURI(webAnnotation.getId()))// .link(RESOURCE_TYPE_URI, "type")// .link(ANNOTATION_TYPE_URI, "type")// .tag(webAnnotation.eTag())// .allow(ALLOWED_METHODS)// .entity(profiledWebAnnotation)// .build(); } @GET @Path("{uuid}") @Produces(JSONLD_MEDIATYPE) public Response getWebAnnotation(// @HeaderParam("Accept") String acceptHeader, // @PathParam("uuid") UUIDParam uuidParam // ) { String expectedProfile = extractProfile(acceptHeader); AlexandriaAnnotation alexandriaAnnotation = service.readAnnotation(uuidParam.getValue()) // .orElseThrow(annotationNotFoundForId(uuidParam)); if (alexandriaAnnotation.getState().equals(AlexandriaState.DELETED)) { return Response.status(Status.GONE).build(); } WebAnnotation webAnnotation = asWebAnnotation(alexandriaAnnotation); String profiledWebAnnotation = profile(webAnnotation.json(), expectedProfile); return Response.ok(profiledWebAnnotation)// .link(RESOURCE_TYPE_URI, "type")// .link(ANNOTATION_TYPE_URI, "type")// .tag(webAnnotation.eTag())// .allow(ALLOWED_METHODS)// .build(); } @PUT @Path("{uuid}") @Consumes(JSONLD_MEDIATYPE) @Produces(JSONLD_MEDIATYPE) public Response updateWebAnnotation(// @HeaderParam("Accept") String acceptHeader, // @PathParam("uuid") UUIDParam uuidParam, // WebAnnotationPrototype prototype// ) { // TODO: what if the resource changes? String expectedProfile = extractProfile(acceptHeader); Instant modificationInstant = Instant.now(); prototype.setModified(modificationInstant.toString()); UUID annotationUuid = uuidParam.getValue(); AlexandriaAnnotation alexandriaAnnotation = service.readAnnotation(annotationUuid) // .orElseThrow(annotationNotFoundForId(uuidParam)); try { String json = new ObjectMapper().writeValueAsString(prototype); TentativeAlexandriaProvenance provenance = new TentativeAlexandriaProvenance(ThreadContext.getUserName(), modificationInstant, AlexandriaProvenance.DEFAULT_WHY); AlexandriaAnnotationBody body = service.findAnnotationBodyWithTypeAndValue(WEBANNOTATION_TYPE, json)// .orElseGet(() -> new AlexandriaAnnotationBody(UUID.randomUUID(), WEBANNOTATION_TYPE, json, provenance)); AlexandriaAnnotation newAnnotation = new AlexandriaAnnotation(annotationUuid, body, provenance); alexandriaAnnotation = service.deprecateAnnotation(annotationUuid, newAnnotation); WebAnnotation webAnnotation = asWebAnnotation(alexandriaAnnotation); String profiledWebAnnotation = profile(webAnnotation.json(), expectedProfile); return Response.ok(profiledWebAnnotation)// .link(RESOURCE_TYPE_URI, "type")// .link(ANNOTATION_TYPE_URI, "type")// .tag(webAnnotation.eTag())// .allow(ALLOWED_METHODS)// .build(); } catch (JsonProcessingException e) { e.printStackTrace(); throw new RuntimeException(e); } } @DELETE @Path("{uuid}") public Response deleteWebAnnotation(@PathParam("uuid") UUIDParam uuidParam) { UUID annotationUUID = uuidParam.getValue(); AlexandriaAnnotation annotation = service.readAnnotation(annotationUUID)// .orElseThrow(NotFoundException::new); service.deleteAnnotation(annotation); return Response.noContent().build(); } // @GET // @Produces(JSONLD_MEDIATYPE) // public Response getAllWebAnnotations() { // // TODO // return Response.ok().build(); // } @GET @Path("search") @Produces(JSONLD_MEDIATYPE) public Response getSearchResults(@QueryParam("uri") String uri) { List<Object> webannotations = findWebAnnotationsAbout(uri); return Response.ok(webannotations).build(); } // private methods private String profile(String json, String expectedProfile) { try { Map<String, Object> jsonObject = (Map<String, Object>) JsonUtils.fromString(json); JsonLdOptions options = new JsonLdOptions(); List<Object> expanded = JsonLdProcessor.expand(jsonObject, options); Map<String, Object> profiled = JsonLdProcessor.compact(expanded, expectedProfile, options); profiled.put("@context", expectedProfile); return new ObjectMapper().writeValueAsString(profiled); } catch (IOException | JsonLdError e) { throw new RuntimeException(e); } } private List<Object> findWebAnnotationsAbout(String uri) { AlexandriaQuery query = new AlexandriaQuery()// .setPageSize(1000)// .setFind("annotation")// .setWhere("type:eq(\"" + WEBANNOTATION_TYPE + "\") resource.ref:eq(\"" + uri + "\")")// .setReturns("id,value"); SearchResult result = service.execute(query); List<Object> webannotations = Lists.newArrayList(); result.getResults().forEach(resultMap -> { String json = (String) resultMap.get("value"); UUID uuid = UUID.fromString((String) resultMap.get("id")); try { Map<String, Object> map = enrichJson(json); map.put("@id", webAnnotationService.webAnnotationURI(uuid)); webannotations.add(map); } catch (IOException e) { throw new RuntimeException(e); } }); return webannotations; } private Map<String, Object> enrichJson(String json) throws IOException { Map<String, Object> map = new ObjectMapper().readValue(json, Map.class); return map; } private static final Pattern PROFILE_PATTERN = Pattern.compile(".*profile=\"(.*?)\".*"); private String extractProfile(String accept) { Matcher matcher = PROFILE_PATTERN.matcher(accept); if (matcher.matches()) { return matcher.group(1); } return DEFAULT_PROFILE; } private WebAnnotation asWebAnnotation(AlexandriaAnnotation alexandriaAnnotation) { AlexandriaAnnotationBody body = alexandriaAnnotation.getBody(); String type = body.getType(); String value = body.getValue(); String json = ""; Instant when = alexandriaAnnotation.getProvenance().getWhen(); if (WEBANNOTATION_TYPE.equals(type)) { json = addIdToValue(alexandriaAnnotation, value); } else { Map<String, Object> webAnnotationMap = Maps.newHashMap(); webAnnotationMap.put("@context", "http://www.w3.org/ns/anno.jsonld"); webAnnotationMap.put("@id", locationBuilder.locationOf(alexandriaAnnotation)); webAnnotationMap.put("type", "Annotation"); webAnnotationMap.put("created", when.toString()); Map<String, String> bodyMap = ImmutableMap.of("type", "TextualBody", "value", type + ": " + value); webAnnotationMap.put("body", bodyMap); webAnnotationMap.put("target", locationBuilder.locationOf(alexandriaAnnotation.getAnnotatablePointer())); try { json = new ObjectMapper().writeValueAsString(webAnnotationMap); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } return new WebAnnotation(alexandriaAnnotation.getId())// .setJson(json)// .setETag(String.valueOf(when.hashCode())); } private String addIdToValue(AlexandriaAnnotation alexandriaAnnotation, String value) { String json = value; try { Map<String, Object> webAnnotationMap = enrichJson(json); webAnnotationMap.put("@id", webAnnotationService.webAnnotationURI(alexandriaAnnotation.getId())); json = new ObjectMapper().writeValueAsString(webAnnotationMap); } catch (IOException e) { throw new RuntimeException(e); } return json; } static Supplier<NotFoundException> annotationNotFoundForId(Object id) { return () -> new NotFoundException(NoAnnotationFoundWithId(id)); } private static String NoAnnotationFoundWithId(Object id) { return "No annotation found with id " + id; } }